// 配置子目录：需要编辑内容有：SUKTAP_ROOT 和 各模板文件

// 引入模块
const http = require('http');
const fs = require('fs');
// const path = require('path'); // 发现似乎不需要
const querystring = require('querystring');

// 这些内容其实应该可以利用 dotenv 修改实现……不过要另外的依赖，我就懒得搞了。
const TWEETS_PER_PAGE = 20;				// 每页显示的留言数量
const CHARSET="utf-8";						// 默认编码
const DATAFILE="data.json";				// 数据档案（JSON）
const ERRLOG="logs/error.log"			// 错误日志
const ACCLOG="logs/access.log"		// 运行日记
const PORT = 9999;								// 监听端口
const SUKTAP_ROOT="";							// 末尾不需要加上“/”
const C32EXC = 8;									// 这里调整IP摘要变数，设定为 8 时返回 CRC32

// 未来期望：
// 1. 加入强制性 Cookie 设定
// 2. 浏览器指纹讯息。
// 3. 在配置定义提交成功或失败的返回模板（最最简单的）
// 4. 附件

const mimeTypes = { // Mimetypes 表，可按需增删
	'html': 'text/html',
	'js': 'text/javascript',
	'css': 'text/css',
	'json': 'application/json',
	'png': 'image/png',
	'jpg': 'image/jpg',
	'gif': 'image/gif',
	'svg': 'application/image/svg+xml'
};

// 创建HTTP服务器
const server = http.createServer((req, res) => {
	const target = req.url.split("?")[0].split("#")[0].replace(/\/$/i,'');
	const reqip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; // 获取造访 IP
	// console.log(target,reqip,req.url); // 调试用

	// 日志（…………可改进）
	fs.appendFileSync(ACCLOG,get_date()+"	"+reqip+"	"+req.method+"	"+req.url+"\n");
	// CSP 政策——可以部分避免 XSS 攻击者得逞
	res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'sha256-8f4175e388db17a65f88d76c7fbd5c94ffdde5a312db483e3cfc8bc49cc0a1ad'; object-src 'none'");
	// 返回头
	res.setHeader("Content-Type", `text/html; charset=${CHARSET}`);
	// 造访，白名单优先，其次是其它文件
	if(target==SUKTAP_ROOT){
		// 首页
		// 首页的套模板需要特殊对待，不使用 pack 函数。
		res.writeHead(200,{'Content-Type':`text/html;charset=${CHARSET}`});
		res.write(load("./head.html",`${CHARSET}`).replace(/{{ip}}/g,reqip).replace(/{{time}}/g,get_date()));
		const data=load(`./${DATAFILE}`,CHARSET);
		const tweet=JSON.parse(data);
		// 处理留言资料
		query = tweet.length < TWEETS_PER_PAGE ? tweet.length : TWEETS_PER_PAGE; // 鉴定大小
		for(var a=0;a<query;a++){
			if(!tweet[a].content){break;}
			// 附带：基础处理。（这里不加对抗XSS，因为模板和服务器响应头已经对浏览器要求了，主流浏览器都支援有关设定）
			// 另外，我相信大部分应用防火墙应该可以拦截恶意造访者提交有害内容。
			var email='';if(tweet[a].email){email=mailparse(`${tweet[a].email}`)}
			var author=tweet[a].from;
			var content=tweet[a].content;
			var date=tweet[a].date;
			var ip=tweet[a].ip;
			
			// 模板填写
			res.write(
				load("./tweet.html",`${CHARSET}`)
				.replace(/{{作者}}/gi,author)
				.replace(/{{内容}}/gi,content)
				.replace(/{{ipcrc32}}/gi,gethash(ip).toUpperCase())
				.replace(/{{邮箱}}/gi,email)
				.replace(/{{时间}}/gi,date)
			);
		}
		res.write(load("./foot.html",`${CHARSET}`).replace(/{{ip}}/g,reqip));
		res.end();
	}
	// 发表留言
	else if(target==`${SUKTAP_ROOT}/write.hsp`){
		res.writeHead(200,{'Content-Type':`text/html;charset=${CHARSET}`});
		pack(res,"write.html",reqip);
		res.end();
	}
	// 接受提交
	else if(target==`${SUKTAP_ROOT}/submit.hsp`){
		if (req.method === 'POST') {
			let body = '';
			req.on('data', chunk => {body += chunk;});
			req.on('end', () => {
				const postData = querystring.parse(body);
				try{
					var author  = pre(postData.from);
					var email   = pre(postData.mail);
					var content = pre(postData.content);
					var ip      = reqip;
				}catch{
					pack(res,"submitfailed.html",reqip);
					return;
				}
				const newContent = `{"from":"${author}","email":"${email}","ip":"${ip}","date":"${get_date()}","content":"${content}"},`;
				// 处理文件
				try {
					content = fs.readFileSync(DATAFILE,CHARSET);
					const lines = content.split('\n');
					lines.splice(1,0,newContent);
					const updatedContent = lines.join('\n');
					fs.writeFileSync(DATAFILE,updatedContent,CHARSET);
					pack(res,"submitfinish.html",reqip);
					res.end();
				} catch (err) {
					console.log(err)
					fs.writeFileSync(ERRLOG,err,CHARSET);
					pack(res,"submitfailed.html",reqip);
					res.end();
				}
			});
		}else{
			pack(res,"submitfailed.html",reqip);
			res.end();
		}
		setTimeout(()=>{res.end();},1500);
	}
	// 绍介
	else if(target==`${SUKTAP_ROOT}/about.hsp`){
		pack(res,"about.html",reqip);
		res.end();
	}
	// 其它HSP
	else if(target.match(/\.hsp$/i)){
		try{
			const stats=fs.statSync("./"+target);
			if (stats.isDirectory()) {
				err404(res);
			}else if(stats.isFile()){
				res.writeHead(200,{'Content-Type':`text/html;charset=${CHARSET}`});
				pack(res,"./"+target,reqip);
				res.end();
			}else{
				err404(res);
			}
		}catch{
			err404(res);
		}
	}
	// 拦截直接读取数据行为。最好是 404 谎称档案不存在。
	else if(target==`${SUKTAP_ROOT}/${DATAFILE}`){
		err404(res);
	}
	// 其余的文件
	else{
		try{
			const stats=fs.statSync("./"+target);
			if (stats.isDirectory()) {
				err404(res);
			}else if(stats.isFile()){
				const contentType = mimeTypes[target.split(".")[(target.split(".").length)-1]] || 'application/octet-stream';
				res.writeHead(200,{'Content-Type':contentType});
				res.write(fs.readFileSync("./"+target));
				res.end();
			}else{
				err404(res);
			}
		}catch{
			err404(res);
		}
	}
	// 避免意外。超时可以变更
	setTimeout(()=>{res.end();},1500);
	return;
});


server.listen(PORT, () => {
	// console.log(`服务器正在运行在 http://localhost:${PORT}`); // 调试用
});

function err404(res){
	res.writeHead(404,{'Content-Type':`text/html; charset=${CHARSET}`});
	// res.write(load("404.html")); // 如果有需要的话，使用res.write写入 404 内容
	res.end();
}
function load(target){
	return fs.readFileSync(target,`${CHARSET}`)
}
function mailparse(mail){
	if(mail.match(/[><%$#!^&*\\\/+"'\[\]{}\|,`\~\(\)]/g)){return '';} // 有不合法字符时拒绝
	return `&lt;<a href="mailto:${mail}">${mail}</a>&gt;`
}
function pre(str){
	// 非常基础的 XSS 等不合法元素预防
	if(str.match(/(<script|<button|<input|<textarea|<iframe|<frame|<select|<option|onload=|onerror=)/i)){
		throw new Error('Not allowed');
	}
	// 简单替换容易引起歧义的字符——换行符导致JSON读取报错；&导致网页意外解析内容；
	return str.replace(/(\r\n|\n\r|\n|\r)/g,'<br/>').replace(/\\/g,'\\\\').replace(/"/g,'\\"').replace('&','&amp;');
}
function get_date(format="message"){  // 报日期专员
	var gdate=new Date();
	if(format=="message"){return gdate.getFullYear()+"-"+(gdate.getMonth()+1)+"-"+gdate.getDate()+" "+gdate.getHours()+":"+gdate.getMinutes()+":"+gdate.getSeconds();}
	else{return gdate;}
}
function pack(res,target,reqip=null){  // 包装专员
	res.write(load("./head.html",`${CHARSET}`).replace('{{ip}}',reqip).replace(/{{time}}/g,get_date()));
	if(target){res.write(load(`./${target}`));}
	res.write(load("./foot.html",`${CHARSET}`).replace('{{ip}}',reqip).replace(/{{time}}/g,get_date()));
}
// 以下是获取 CRC32 （但是修改）的 
function gethash(str) {
	let crcTable = [];
	for (let i = 0; i < 256; i++) {
		let c = i;
		for (let j = 0; j < C32EXC ; j++) {
			c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
		}
		crcTable[i] = c;
	}

	let crc = 0 ^ (-1);
	for (let i = 0; i < str.length; i++) {
		crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
	}
	return ((crc ^ (-1)) >>> 0).toString(16);
}
